/**
 * Copyright (C) 2014 Paul Cech
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.eazegraph.lib.charts;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorValue;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.ScrollHelper;
import ohos.agp.render.*;
import ohos.agp.utils.Color;
import ohos.agp.utils.Matrix;
import ohos.agp.utils.Point;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.multimodalinput.event.TouchEvent;
import org.eazegraph.lib.ValueAnimator;
import org.eazegraph.lib.communication.IOnPointFocusedListener;
import org.eazegraph.lib.gesture.GestureDetector;
import org.eazegraph.lib.gesture.ScaleGestureDetector;
import org.eazegraph.lib.models.BaseModel;
import org.eazegraph.lib.models.LegendModel;
import org.eazegraph.lib.models.Point2D;
import org.eazegraph.lib.models.StandardValue;
import org.eazegraph.lib.models.ValueLinePoint;
import org.eazegraph.lib.models.ValueLineSeries;
import org.eazegraph.lib.utils.AttrUtils;
import org.eazegraph.lib.utils.LogUtil;
import org.eazegraph.lib.utils.ScaleGestureDetectorCompat;
import org.eazegraph.lib.utils.Utils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * A LineChart which displays various line series with one value and the remaining information is
 * calculated dynamically. It is possible to draw normal and cubic lines.
 */
public class ValueLineChart extends BaseChart {
    /**
     * * The constant DEF_USE_CUBIC
     */
    public static final boolean DEF_USE_CUBIC = false;
    /**
     * * The constant DEF_USE_OVERLAP_FILL
     */
    public static final boolean DEF_USE_OVERLAP_FILL = false;
    /**
     * * The constant DEF_LINE_STROKE
     */
    public static final float DEF_LINE_STROKE = 5f;
    /**
     * * The constant DEF_FIRST_MULTIPLIER
     */
    public static final float DEF_FIRST_MULTIPLIER = 0.33f;
    /**
     * * The constant DEF_SHOW_INDICATOR
     */
    public static final boolean DEF_SHOW_INDICATOR = true;
    /**
     * * The constant DEF_INDICATOR_WIDTH
     */
    public static final float DEF_INDICATOR_WIDTH = 2f;
    /**
     * * The constant DEF_INDICATOR_COLOR
     */
    public static final int DEF_INDICATOR_COLOR = 0xFF0000FF;
    /**
     * * The constant DEF_INDICATOR_TEXT_SIZE will be interpreted as sp value
     */
    public static final float DEF_INDICATOR_TEXT_SIZE = 15.f;
    /**
     * * The constant DEF_INDICATOR_LEFT_PADDING
     */
    public static final float DEF_INDICATOR_LEFT_PADDING = 4.f;
    /**
     * * The constant DEF_INDICATOR_TOP_PADDING
     */
    public static final float DEF_INDICATOR_TOP_PADDING = 4.f;
    /**
     * * The constant DEF_SHOW_STANDARD_VALUE
     */
    public static final boolean DEF_SHOW_STANDARD_VALUE = true;
    /**
     * * The constant DEF_X_AXIS_STROKE
     */
    public static final float DEF_X_AXIS_STROKE = 2f;
    /**
     * * The constant DEF_LEGEND_STROKE
     */
    public static final float DEF_LEGEND_STROKE = 2f;
    /**
     * * The constant DEF_ACTIVATE_INDICATOR_SHADOW
     */
    public static final boolean DEF_ACTIVATE_INDICATOR_SHADOW = false;
    /**
     * * The constant DEF_INDICATOR_SHADOW_STRENGTH dimension value
     */
    public static final float DEF_INDICATOR_SHADOW_STRENGTH = 0.7f;
    /**
     * * The constant DEF_INDICATOR_SHADOW_COLOR
     */
    public static final int DEF_INDICATOR_SHADOW_COLOR = 0xFF676767;
    /**
     * * The constant DEF_INDICATOR_TEXT_UNIT
     */
    public static final String DEF_INDICATOR_TEXT_UNIT = "";
    /**
     * * The constant DEF_SHOW_LEGEND_BENEATH_INDICATOR
     */
    public static final boolean DEF_SHOW_LEGEND_BENEATH_INDICATOR = false;
    /**
     * * The constant DEF_USE_DYNAMIC_SCALING
     */
    public static final boolean DEF_USE_DYNAMIC_SCALING = false;
    /**
     * * The constant DEF_SCALING_FACTOR
     */
    public static final float DEF_SCALING_FACTOR = 0.96f;
    /**
     * * The constant DEF_MAX_ZOOM_X
     */
    public static final float DEF_MAX_ZOOM_X = 3.f;
    /**
     * * The constant DEF_MAX_ZOOM_Y
     */
    public static final float DEF_MAX_ZOOM_Y = 3.f;
    /**
     * * The constant TAG
     */
    private static final String TAG = "ValueLineChart";
    /**
     * * The constant LOG_TAG
     */
    private static final String LOG_TAG = ValueLineChart.class.getSimpleName();
    /**
     * Constructor that is called when inflating a view from XML. This is called
     * when a view is being constructed from an XML file, supplying attributes
     * that were specified in the XML file. This version uses a default style of
     * 0, so the only attribute values applied are those in the Context's Theme
     * and the given AttributeSet.
     * <p/>
     * <p/>
     * The method onFinishInflate() will be called after all children have been
     * added.
     *
     * @param context The Context the view is running in, through which it can access the current theme, resources, etc.
     * @param attrs The attributes of the XML tag that is inflating the view.
     * @see
     */
    private final String ValueLineChart_egIndicatorShadowStrength = "egIndicatorShadowStrength";
    /**
     * * The constant ValueLineChart_egIndicatorShadowColor
     */
    private final String ValueLineChart_egIndicatorShadowColor = "egIndicatorShadowColor";
    /**
     * * The constant ValueLineChart_egIndicatorTextUnit
     */
    private final String ValueLineChart_egIndicatorTextUnit = "egIndicatorTextUnit";
    /**
     * * The constant ValueLineChart_egShowLegendBeneathIndicator
     */
    private final String ValueLineChart_egShowLegendBeneathIndicator = "egShowLegendBeneathIndicator";
    /**
     * * The constant ValueLineChart_egMaxYZoom
     */
    private final String ValueLineChart_egMaxYZoom = "egMaxYZoom";
    /**
     * * The constant ValueLineChart_egUseDynamicScaling
     */
    private final String ValueLineChart_egUseDynamicScaling = "egUseDynamicScaling";
    /**
     * * The constant ValueLineChart_egScalingFactor
     */
    private final String ValueLineChart_egScalingFactor = "egScalingFactor";
    /**
     * * The constant ValueLineChart_egMaxXZoom
     */
    private final String ValueLineChart_egMaxXZoom = "egMaxXZoom";
    /**
     * * The constant ValueLineChart_egActivateIndicatorShadow
     */
    private final String ValueLineChart_egActivateIndicatorShadow = "egActivateIndicatorShadow";
    /**
     * * The constant ValueLineChart_egXAxisStroke
     */
    private final String ValueLineChart_egXAxisStroke = "egXAxisStroke";
    /**
     * * The constant ValueLineChart_egShowStandardValue
     */
    private final String ValueLineChart_egShowStandardValue = "egShowStandardValue";
    /**
     * * The constant ValueLineChart_egIndicatorTopPadding
     */
    private final String ValueLineChart_egIndicatorTopPadding = "egIndicatorTopPadding";
    /**
     * * The constant ValueLineChart_egIndicatorLeftPadding
     */
    private final String ValueLineChart_egIndicatorLeftPadding = "egIndicatorLeftPadding";
    /**
     * * The constant ValueLineChart_egIndicatorWidth
     */
    private final String ValueLineChart_egIndicatorWidth = "egIndicatorWidth";
    /**
     * * The constant ValueLineChart_egIndicatorTextColor
     */
    private final String ValueLineChart_egIndicatorTextColor = "egIndicatorTextColor";
    /**
     * * The constant ValueLineChart_egIndicatorLineColor
     */
    private final String ValueLineChart_egIndicatorLineColor = "egIndicatorLineColor";
    /**
     * * The constant ValueLineChart_egShowValueIndicator
     */
    private final String ValueLineChart_egShowValueIndicator = "egShowValueIndicator";
    /**
     * * The constant ValueLineChart_egCurveSmoothness
     */
    private final String ValueLineChart_egCurveSmoothness = "egCurveSmoothness";
    /**
     * * The constant ValueLineChart_egLineStroke
     */
    private final String ValueLineChart_egLineStroke = "egLineStroke";
    /**
     * * The constant ValueLineChart_egUseOverlapFill
     */
    private final String ValueLineChart_egUseOverlapFill = "egUseOverlapFill";
    /**
     * * The constant ValueLineChart_egUseCubic
     */
    private final String ValueLineChart_egUseCubic = "egUseCubic";
    /**
     * The constant M draw matrix
     */
    protected Matrix mDrawMatrix = new Matrix();
    /**
     * The gesture listener, used for handling simple gestures such as double touches, scrolls,
     * and flings.
     */
    private float[] mat = new float[]{1f, 0f, 0f,
            0f, 1f, 0f,
            0f, 0f, 1f};
    /**
     * The constant Mat 1
     */
    private float[] mat1 = new float[]{1f, 0f, 0f,
            0f, 1f, 0f,
            0f, 0f, 1f};
    /**
     * The constant M line paint
     */
    private Paint mLinePaint;
    /**
     * The constant M legend paint
     */
    private Paint mLegendPaint;
    /**
     * The constant M indicator paint
     */
    private Paint mIndicatorPaint;
    /**
     * This is used to have a little extra space on the top, so if a standard value is added
     * and it is the biggest value, preventing the standard value line being cropped out a bit.
     */
    private int mUsableGraphHeight;
    /**
     * The constant M graph height padding
     */
    private float mGraphHeightPadding = Utils.dpToPx(getContext(),2.f);
    /**
     * The constant M series
     */
    private List<ValueLineSeries> mSeries;
    /**
     * The constant M legend list
     */
    private List<LegendModel> mLegendList;
    /**
     * The constant M has negative values
     */
    private boolean mHasNegativeValues = false;
    /**
     * The constant M negative value
     */
    private float mNegativeValue = 0.f;
    /**
     * The constant M negative offset
     */
    private float mNegativeOffset = 0.f;
    /**
     * The constant M listener
     */
    private IOnPointFocusedListener mListener = null;
    /**
     * The constant M first multiplier
     */
    private float mFirstMultiplier;
    /**
     * The constant M second multiplier
     */
    private float mSecondMultiplier;
    /**
     * The constant M use custom legend
     */
    private boolean mUseCustomLegend = false;
    /**
     * The constant M touched area
     */
    private Point2D mTouchedArea = new Point2D(0, 0);
    /**
     * The constant M focused point
     */
    private ValueLinePoint mFocusedPoint = null;
    /**
     * The constant M value text height
     */
    private float mValueTextHeight;
    /**
     * The constant M last point GraphOverlay vars
     */
    private ValueLinePoint mLastPoint = null;
    /**
     * The constant M value label x
     */
    private int mValueLabelX = 0;
    /**
     * The constant M value label y
     */
    private int mValueLabelY = 0;
    /**
     * The constant M legend label x
     */
    private int mLegendLabelX = 0;
    /**
     * The constant M legend label y
     */
    private int mLegendLabelY = 0;
    /**
     * The constant M standard values
     */
    private List<StandardValue> mStandardValues = new ArrayList<StandardValue>();
    /**
     * Indicates to fill the bottom area of a series with its given color.
     */
    private boolean mUseOverlapFill;
    /**
     * The constant M use cubic
     */
    private boolean mUseCubic;
    /**
     * The constant M line stroke
     */
    private float mLineStroke;
    /**
     * The constant M show indicator
     */
    private boolean mShowIndicator;

    /**
     * Implement this to do your drawing.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    private float mIndicatorWidth;
    /**
     * The constant M indicator line color
     */
    private int mIndicatorLineColor;
    /**
     * The constant M indicator text color
     */
    private int mIndicatorTextColor;
    /**
     * The constant M indicator text size
     */
    private float mIndicatorTextSize;
    /**
     * The constant M indicator left padding
     */
    private float mIndicatorLeftPadding;
    /**
     * The constant M indicator top padding
     */
    private float mIndicatorTopPadding;
    /**
     * The constant M show standard values
     */
    private boolean mShowStandardValues;
    /**
     * The constant M x axis stroke
     */
    private float mXAxisStroke;
    /**
     * The constant M activate indicator shadow
     */
    private boolean mActivateIndicatorShadow;
    /**
     * The constant M indicator shadow strength
     */
    private float mIndicatorShadowStrength;
    /**
     * The constant M indicator shadow color
     */
    private int mIndicatorShadowColor;
    /**
     * The constant M indicator text unit
     */
    private String mIndicatorTextUnit;
    /**
     * The constant M show legend beneath indicator
     */
    private boolean mShowLegendBeneathIndicator;
    /**
     * Enabling this when only positive and big values are present and only have little fluctuations,
     * a y-axis scaling takes place to see a better difference between the values.
     */
    private boolean mUseDynamicScaling;
    /**
     * The factor for the dynamic scaling, which determines how many percent of the minimum value
     * should be subtracted to achieve the scaling.
     */
    private float mScalingFactor;
    /**
     * The constant M max zoom x
     */
    private float mMaxZoomX = DEF_MAX_ZOOM_X;
    /**
     * The constant M max zoom y
     */
    private float mMaxZoomY = DEF_MAX_ZOOM_Y;
    /**
     * The constant M draw matrix values
     */
    private float[] mDrawMatrixValues = new float[]{1f, 0f, 0f,
            0f, 1f, 0f,
            0f, 0f, 1f};

    /**
     * The constant M is interacting
     */
    private boolean mIsInteracting = false;
    /**
     * The scale listener, used for handling multi-finger scale gestures.
     */
    private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener
            = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
        float mLastFocusX;
        float mLastFocusY;

        boolean mZoomIn;

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            mLastFocusX = detector.getFocusX();
            mLastFocusY = detector.getFocusY();
            return true;
        }

        @Override
        public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
            mIsInteracting = true;
            Matrix transformationMatrix = new Matrix();
            float focusX = scaleGestureDetector.getFocusX();
            float focusY = scaleGestureDetector.getFocusY();
            if (ScaleGestureDetectorCompat.getPreviousSpanX(scaleGestureDetector) == 0 || ScaleGestureDetectorCompat.getPreviousSpanY(scaleGestureDetector) == 0) {
                return false;
            }

            float scaleX = ScaleGestureDetectorCompat.getCurrentSpanX(scaleGestureDetector) / ScaleGestureDetectorCompat.getPreviousSpanX(scaleGestureDetector);
            float scaleY = ScaleGestureDetectorCompat.getCurrentSpanY(scaleGestureDetector) / ScaleGestureDetectorCompat.getPreviousSpanY(scaleGestureDetector);

            // Zoom focus is where the fingers are centered,
            transformationMatrix.getElements(mat1);

            transformationMatrix.postTranslate(-focusX, -focusY);

            transformationMatrix.getElements(mat1);
            transformationMatrix.postScale(scaleX, scaleY);

            transformationMatrix.getElements(mat1);
            float focusShiftX = focusX - mLastFocusX;
            float focusShiftY = focusY - mLastFocusY;

            transformationMatrix.getElements(mat1);
            transformationMatrix.postTranslate(focusX + focusShiftX, focusY + focusShiftY);

            transformationMatrix.getElements(mat1);
            mDrawMatrix.postConcat(transformationMatrix);
            constrainView();
            recalculateXCoordinates(mGraphWidth * mDrawMatrixValues[0]);
            if (calculateLegendBounds()) {
                Utils.calculateLegendInformation(getContext(), mSeries.get(0).getSeries(), 0, mGraphWidth * mDrawMatrixValues[0], mLegendPaint);
            }

            mLastFocusX = focusX;
            mLastFocusY = focusY;

            if (mFocusedPoint != null) {
                calculateValueTextHeight();
            }

            invalidateGlobal();

            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            super.onScaleEnd(detector);
        }
    };
    /**
     * The constant M dash path effect
     */
    private PathEffect mDashPathEffect = new PathEffect(new float[]{10, 20}, 0);
    /**
     * The constant M scale gesture detector State objects and values related to gesture tracking.
     */
    private ScaleGestureDetector mScaleGestureDetector;
    /**
     * The constant M gesture detector
     */
    private GestureDetector mGestureDetector;
    /**
     * The constant M scroller
     */
    private ScrollHelper mScroller;
    /**
     * The constant M scroll animator
     */
    private ValueAnimator mScrollAnimator;


    /**
     * The constant M gesture listener
     */
    private final GestureDetector.SimpleOnGestureListener mGestureListener
            = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onScroll(TouchEvent e1, TouchEvent e2, float distanceX, float distanceY) {
            LogUtil.info(TAG, "onScroll: ");
            mIsInteracting = true;

            mDrawMatrix.getElements(mat);
            LogUtil.info(TAG, "onScroll: " + Arrays.toString(mat));
            mDrawMatrix.postTranslate(-distanceX, -distanceY);
            mDrawMatrix.getElements(mat);
            LogUtil.info(TAG, "onScroll: " + Arrays.toString(mat));
            constrainView();

            invalidateGlobal();
            return true;
        }

        @Override
        public boolean onFling(TouchEvent e1, TouchEvent e2, float velocityX, float velocityY) {
            LogUtil.info(TAG, "onFling: ");
            fling((int) -velocityX, (int) -velocityY);
            return true;
        }

        @Override
        public boolean onDown(TouchEvent event) {
            // The user is interacting with the pie, so we want to turn on acceleration
            // so that the interaction is smooth.
            if (!mScroller.isFinished()) {
                stopScrolling();
            }
            return true;
        }
    };

    /**
     * Simple constructor to use when creating a view from code.
     *
     * @param context The Context the view is running in, through which it can                access the current theme, resources, etc.
     */
    public ValueLineChart(Context context) {
        super(context);

        mUseCubic = DEF_USE_CUBIC;
        mUseOverlapFill = DEF_USE_OVERLAP_FILL;
        mLineStroke = Utils.dpToPx(getContext(),DEF_LINE_STROKE);
        mFirstMultiplier = DEF_FIRST_MULTIPLIER;
        mSecondMultiplier = 1.0f - mFirstMultiplier;
        mShowIndicator = DEF_SHOW_INDICATOR;
        mIndicatorWidth = Utils.dpToPx(getContext(),DEF_INDICATOR_WIDTH);
        mIndicatorLineColor = DEF_INDICATOR_COLOR;
        mIndicatorTextColor = DEF_INDICATOR_COLOR;
        mIndicatorTextSize = Utils.dpToPx(getContext(),DEF_INDICATOR_TEXT_SIZE);
        mIndicatorLeftPadding = Utils.dpToPx(getContext(),DEF_INDICATOR_LEFT_PADDING);
        mIndicatorTopPadding = Utils.dpToPx(getContext(),DEF_INDICATOR_TOP_PADDING);
        mShowStandardValues = DEF_SHOW_STANDARD_VALUE;
        mXAxisStroke = Utils.dpToPx(getContext(),DEF_X_AXIS_STROKE);
        mActivateIndicatorShadow = DEF_ACTIVATE_INDICATOR_SHADOW;
        mIndicatorShadowStrength = Utils.dpToPx(getContext(),DEF_INDICATOR_SHADOW_STRENGTH);
        mIndicatorShadowColor = DEF_INDICATOR_SHADOW_COLOR;
        mIndicatorTextUnit = DEF_INDICATOR_TEXT_UNIT;
        mShowLegendBeneathIndicator = DEF_SHOW_LEGEND_BENEATH_INDICATOR;
        mUseDynamicScaling = DEF_USE_DYNAMIC_SCALING;
        mScalingFactor = DEF_SCALING_FACTOR;
        mMaxZoomX = DEF_MAX_ZOOM_X;
        mMaxZoomY = DEF_MAX_ZOOM_Y;

        initializeGraph();
    }

    /**
     * Value line chart
     *
     * @param context context
     * @param attrs   attrs
     */
    public ValueLineChart(Context context, AttrSet attrs) {
        super(context, attrs);
        mUseCubic = AttrUtils.getBooleanFromAttr(attrs, ValueLineChart_egUseCubic, DEF_USE_CUBIC);
        mUseOverlapFill = AttrUtils.getBooleanFromAttr(attrs, ValueLineChart_egUseOverlapFill, DEF_USE_OVERLAP_FILL);
        mLineStroke = AttrUtils.getDimensionFromAttr(attrs, ValueLineChart_egLineStroke, (int) Utils.dpToPx(getContext(),DEF_LINE_STROKE));
        mFirstMultiplier = AttrUtils.getFloatFromAttr(attrs, ValueLineChart_egCurveSmoothness, DEF_FIRST_MULTIPLIER);
        mSecondMultiplier = 1.0f - mFirstMultiplier;
        mShowIndicator = AttrUtils.getBooleanFromAttr(attrs, ValueLineChart_egShowValueIndicator, DEF_SHOW_INDICATOR);
        mIndicatorWidth = AttrUtils.getDimensionFromAttr(attrs, ValueLineChart_egIndicatorWidth, (int) Utils.dpToPx(getContext(),DEF_INDICATOR_WIDTH));
        mIndicatorLineColor = AttrUtils.getColorFromAttr(attrs, ValueLineChart_egIndicatorLineColor, DEF_INDICATOR_COLOR);
        mIndicatorTextColor = AttrUtils.getColorFromAttr(attrs, ValueLineChart_egIndicatorTextColor, DEF_INDICATOR_COLOR);
        mIndicatorTextSize = AttrUtils.getDimensionFromAttr(attrs, ValueLineChart_egIndicatorWidth, (int) Utils.dpToPx(getContext(),DEF_INDICATOR_TEXT_SIZE));
        mIndicatorLeftPadding = AttrUtils.getDimensionFromAttr(attrs, ValueLineChart_egIndicatorLeftPadding, (int) Utils.dpToPx(getContext(),DEF_INDICATOR_LEFT_PADDING));
        mIndicatorTopPadding = AttrUtils.getDimensionFromAttr(attrs, ValueLineChart_egIndicatorTopPadding, (int) Utils.dpToPx(getContext(),DEF_INDICATOR_TOP_PADDING));
        mShowStandardValues = AttrUtils.getBooleanFromAttr(attrs, ValueLineChart_egShowStandardValue, DEF_SHOW_STANDARD_VALUE);
        mXAxisStroke = AttrUtils.getDimensionFromAttr(attrs, ValueLineChart_egXAxisStroke, (int) Utils.dpToPx(getContext(),DEF_X_AXIS_STROKE));
        mActivateIndicatorShadow = AttrUtils.getBooleanFromAttr(attrs, ValueLineChart_egActivateIndicatorShadow, DEF_ACTIVATE_INDICATOR_SHADOW);
        mIndicatorShadowStrength = AttrUtils.getDimensionFromAttr(attrs, ValueLineChart_egIndicatorShadowStrength, (int) Utils.dpToPx(getContext(),DEF_INDICATOR_SHADOW_STRENGTH));
        mIndicatorShadowColor = AttrUtils.getColorFromAttr(attrs, ValueLineChart_egIndicatorShadowColor, DEF_INDICATOR_SHADOW_COLOR);
        mIndicatorTextUnit = AttrUtils.getStringFromAttr(attrs, ValueLineChart_egIndicatorTextUnit, "");
        mShowLegendBeneathIndicator = AttrUtils.getBooleanFromAttr(attrs, ValueLineChart_egShowLegendBeneathIndicator, DEF_SHOW_LEGEND_BENEATH_INDICATOR);
        mUseDynamicScaling = AttrUtils.getBooleanFromAttr(attrs, ValueLineChart_egUseDynamicScaling, DEF_USE_DYNAMIC_SCALING);
        mScalingFactor = AttrUtils.getFloatFromAttr(attrs, ValueLineChart_egScalingFactor, DEF_SCALING_FACTOR);
        mMaxZoomX = AttrUtils.getFloatFromAttr(attrs, ValueLineChart_egMaxXZoom, DEF_MAX_ZOOM_X);
        mMaxZoomY = AttrUtils.getFloatFromAttr(attrs, ValueLineChart_egMaxYZoom, DEF_MAX_ZOOM_Y);

        if (mIndicatorTextUnit == null) {
            mIndicatorTextUnit = "";
        }

        initializeGraph();
    }

    /**
     * Adds a new series to the graph.
     *
     * @param _Series The series which should be added.
     */
    public void addSeries(ValueLineSeries _Series) {
        if (_Series == null) {
            return;
        }        
        mSeries.add(_Series);
        onDataChanged();
    }

    /**
     * Resets and clears the data object.
     */
    @Override
    public void clearChart() {
        mSeries.clear();
        mStandardValues.clear();
        mFocusedPoint = null;
        mLastPoint = null;
    }

    /**
     * Adds a custom legend which should be displayed instead of the dynamic legend.
     *
     * @param _Legend A list of LegendModels which will be displayed.
     */
    public void addLegend(List<LegendModel> _Legend) {
        mLegendList.addAll(_Legend);
        onLegendDataChanged();
    }

    /**
     * Is use custom legend boolean
     *
     * @return the boolean
     */
    public boolean isUseCustomLegend() {
        return mUseCustomLegend;
    }

    /**
     * Set use custom legend *
     *
     * @param _useCustomLegend use custom legend
     */
    public void setUseCustomLegend(boolean _useCustomLegend) {
        mUseCustomLegend = _useCustomLegend;
        onLegendDataChanged();
    }

    /**
     * Adds a standard value to the graph. The standard value is a horizontal line as an overlay
     * dependent on the loaded data set.
     *
     * @param _standardValue The value which will be interpreted as a y-coordinate dependent                       on the maximum value of the data set.
     */
    public void addStandardValue(StandardValue _standardValue) {
        mStandardValues.add(_standardValue);
        onDataChanged();
    }

    /**
     * Adds a standard value to the graph. The standard value is a horizontal line as an overlay
     * dependent on the loaded data set.
     *
     * @param _standardValue The value which will be interpreted as a y-coordinate dependent                       on the maximum value of the data set.
     */
    public void addStandardValue(float _standardValue) {
        mStandardValues.add(new StandardValue(getContext(),_standardValue));
        onDataChanged();
    }

    /**
     * Adds a list of standard values to the graph.
     *
     * @param _standardValues The list with standard values.
     */
    public void addStandardValues(List<StandardValue> _standardValues) {
        mStandardValues.addAll(_standardValues);
        onDataChanged();
    }

    /**
     * Clears the list which contains all standard values.
     */
    public void clearStandardValues() {
        mStandardValues.clear();
        onDataChanged();
    }

    /**
     * Sets the onPointFocusedListener.
     *
     * @param _listener An instance of the IOnPointFocusedListener interface.
     */
    public void setOnPointFocusedListener(IOnPointFocusedListener _listener) {
        mListener = _listener;
    }

    /**
     * Checks if the graph is a cubic graph.
     *
     * @return True if it's a cubic graph.
     */
    public boolean isUseCubic() {
        return mUseCubic;
    }

    /**
     * Sets the option if the graph should use a cubic spline interpolation or not.
     *
     * @param _useCubic True if the graph should use cubic spline interpolation.
     */
    public void setUseCubic(boolean _useCubic) {
        mUseCubic = _useCubic;
        onDataChanged();
    }

    /**
     * Checks if the graph uses an overlap fill. An overlap fill occurs whether the user set it explicitly
     * through the attributes or if only one data set is present.
     *
     * @return True if overlap fill is activated.
     */
    public boolean isUseOverlapFill() {
        return mUseOverlapFill;
    }

    /**
     * Sets the overlap fill attribute.
     *
     * @param _useOverlapFill True if an overlap fill should be used.
     */
    public void setUseOverlapFill(boolean _useOverlapFill) {
        mUseOverlapFill = _useOverlapFill;
        onDataChanged();
    }

    /**
     * Returns the size of the line stroke for every series.
     *
     * @return Line stroke in px.
     */
    public float getLineStroke() {
        return mLineStroke;
    }

    /**
     * Checks if the indicator should be shown or not.
     *
     * @return True if the indicator is shown.
     */
    public boolean isShowIndicator() {
        return mShowIndicator;
    }

    /**
     * Sets if the indicator should be shown or not.
     *
     * @param _showIndicator True if the indicator should be shown.
     */
    public void setShowIndicator(boolean _showIndicator) {
        mShowIndicator = _showIndicator;
        invalidateGlobal();
    }

    /**
     * Returns the indicator line width (stroke).
     *
     * @return Indicator line width in px.
     */
    public float getIndicatorWidth() {
        return mIndicatorWidth;
    }

    /**
     * Sets the indicator line width (stroke)
     *
     * @param _indicatorWidth Indicator width as a dp value.
     */
    public void setIndicatorWidth(float _indicatorWidth) {
        mIndicatorWidth = Utils.dpToPx(getContext(),_indicatorWidth);
        invalidateGlobal();
    }

    /**
     * Returns the color of the indicator line.
     *
     * @return Color value.
     */
    public int getIndicatorLineColor() {
        return mIndicatorLineColor;
    }

    /**
     * Sets the indicator line color.
     *
     * @param _indicatorLineColor Indicator line color value
     */
    public void setIndicatorLineColor(int _indicatorLineColor) {
        mIndicatorLineColor = _indicatorLineColor;
        invalidateGraphOverlay();
    }

    /**
     * Returns the color of the indicator text.
     *
     * @return Color value
     */
    public int getIndicatorTextColor() {
        return mIndicatorTextColor;
    }

    /**
     * Sets the indicator text color.
     *
     * @param _indicatorTextColor Indicator text color value
     */
    public void setIndicatorTextColor(int _indicatorTextColor) {
        mIndicatorTextColor = _indicatorTextColor;
        invalidateGraphOverlay();
    }

    /**
     * Returns the indicators value text size.
     *
     * @return Indicator text size.
     */
    public float getIndicatorTextSize() {
        return mIndicatorTextSize;
    }

    /**
     * Returns the left padding for the indicator text.
     *
     * @return Indicator text left padding in px
     */
    public float getIndicatorLeftPadding() {
        return mIndicatorLeftPadding;
    }

    /**
     * Sets the left padding for the indicator text.
     *
     * @param _indicatorLeftPadding Indicator text left padding in dp
     */
    public void setIndicatorLeftPadding(float _indicatorLeftPadding) {
        mIndicatorLeftPadding = Utils.dpToPx(getContext(),_indicatorLeftPadding);
        invalidateGraphOverlay();
    }

    /**
     * Returns the top padding for the indicator text.
     *
     * @return Indicator text top padding in px
     */
    public float getIndicatorTopPadding() {
        return mIndicatorTopPadding;
    }

    /**
     * Sets the top padding for the indicator text.
     *
     * @param _indicatorTopPadding Indicator text top padding in dp
     */
    public void setIndicatorTopPadding(float _indicatorTopPadding) {
        mIndicatorTopPadding = Utils.dpToPx(getContext(),_indicatorTopPadding);
        invalidateGraphOverlay();
    }

    /**
     * Checks if the standard value line should be shown or not.
     *
     * @return True if the standard value line should be shown.
     */
    public boolean isShowStandardValues() {
        return mShowStandardValues;
    }

    /**
     * Sets if the standard value should be shown or not.
     *
     * @param _showStandardValues True if the standard value line should be shown.
     */
    public void setShowStandardValues(boolean _showStandardValues) {
        mShowStandardValues = _showStandardValues;
        onDataChanged();
    }

    /**
     * Returns the stroke size of the X-axis.
     *
     * @return Stroke size in px.
     */
    public float getXAxisStroke() {
        return mXAxisStroke;
    }

    /**
     * Sets the stroke size of the X-axis.
     *
     * @param _XAxisStroke Stroke size in dp.
     */
    public void setXAxisStroke(float _XAxisStroke) {
        mXAxisStroke = Utils.dpToPx(getContext(),_XAxisStroke);
        invalidateGraphOverlay();
    }

    /**
     * Has activate indicator shadow boolean
     *
     * @return Checks if the shadow layer for the indicator text is activated.
     */
    public boolean hasActivateIndicatorShadow() {
        return mActivateIndicatorShadow;
    }

    /**
     * Toggles the shadow layer for the indicator text.
     *
     * @param _activateIndicatorShadow Indication if the shadow layer should be enabled or not.
     */
    public void setActivateIndicatorShadow(boolean _activateIndicatorShadow) {
        mActivateIndicatorShadow = _activateIndicatorShadow;
        invalidateGraphOverlay();
    }

    /**
     * Get indicator shadow strength float
     *
     * @return The shadow layers strength/radius.
     */
    public float getIndicatorShadowStrength() {
        return mIndicatorShadowStrength;
    }

    /**
     * Sets the shadow layers strength/radius.
     *
     * @param _indicatorShadowStrength The shadow strength/radius (in dp)
     */
    public void setIndicatorShadowStrength(float _indicatorShadowStrength) {
        mIndicatorShadowStrength = Utils.dpToPx(getContext(),_indicatorShadowStrength);
        invalidateGraphOverlay();
    }

    /**
     * Get indicator shadow color int
     *
     * @return The shadow color
     */
    public int getIndicatorShadowColor() {
        return mIndicatorShadowColor;
    }

    /**
     * Sets the color in which the shadow layer will be drawn.
     *
     * @param _indicatorShadowColor Color for the shadow.
     */
    public void setIndicatorShadowColor(int _indicatorShadowColor) {
        mIndicatorShadowColor = _indicatorShadowColor;
        invalidateGraphOverlay();
    }

    /**
     * Get indicator text unit string
     *
     * @return The currently set unit which is placed after the indicator text.
     */
    public String getIndicatorTextUnit() {
        return mIndicatorTextUnit;
    }

    /**
     * Sets the unit which is placed after the indicator text.
     * If it is an empty String, nothing will be displayed and disables the drawing of the unit.
     *
     * @param _indicatorTextUnit The unit which should be drawn.
     */
    public void setIndicatorTextUnit(String _indicatorTextUnit) {
        mIndicatorTextUnit = _indicatorTextUnit;
        invalidateGraphOverlay();
    }

    /**
     * Is use dynamic scaling boolean
     *
     * @return the boolean
     */
    public boolean isUseDynamicScaling() {
        return mUseDynamicScaling;
    }

    /**
     * Set use dynamic scaling *
     *
     * @param _useDynamicScaling use dynamic scaling
     */
    public void setUseDynamicScaling(boolean _useDynamicScaling) {
        mUseDynamicScaling = _useDynamicScaling;
        onDataChanged();
    }

    /**
     * Get scaling factor float
     *
     * @return the float
     */
    public float getScalingFactor() {
        return mScalingFactor;
    }

    /**
     * Set scaling factor *
     *
     * @param _scalingFactor scaling factor
     */
    public void setScalingFactor(float _scalingFactor) {
        mScalingFactor = _scalingFactor;
        onDataChanged();
    }

    /**
     * Get max zoom x float
     *
     * @return the float
     */
    public float getMaxZoomX() {
        return mMaxZoomX;
    }

    /**
     * Get max zoom y float
     *
     * @return the float
     */
    public float getMaxZoomY() {
        return mMaxZoomY;
    }

    /**
     * This is called during layout when the size of this view has changed. If
     * you were just added to the view hierarchy, you're called with the old
     * values of 0.
     *
     * @param component component
     */
    @Override
    public void onRefreshed(Component component) {
        super.onRefreshed(component);
        mUsableGraphHeight = (int) (mGraphHeight - mGraphHeightPadding);

        onDataChanged();
        if (mUseCustomLegend) {
            onLegendDataChanged();
        }
    }

    /**
     * This is the main entry point after the graph has been inflated. Used to initialize the graph
     * and its corresponding members.
     */
    @Override
    protected void initializeGraph() {
        super.initializeGraph();

        mDrawMatrix.setElements(mDrawMatrixValues);
        mGraphOverlay.decelerate();

        mSeries = new ArrayList<ValueLineSeries>();
        mLegendList = new ArrayList<LegendModel>();

        mLinePaint = new Paint();
        mLinePaint.setAntiAlias(true);
        mLinePaint.setStrokeWidth(mLineStroke);

        mLegendPaint = new Paint();
        mLegendPaint.setAntiAlias(true);
        mLegendPaint.setColor(new Color(mLegendColor));
        mLegendPaint.setTextSize((int) mLegendTextSize);
        mLegendPaint.setStrokeWidth(2);
        mLegendPaint.setStyle(Paint.Style.FILL_STYLE);

        mMaxFontHeight = Utils.calculateMaxTextHeight(mLegendPaint, null);

        mIndicatorPaint = new Paint();
        mIndicatorPaint.setAntiAlias(true);
        mIndicatorPaint.setColor(new Color(mIndicatorLineColor));
        mIndicatorPaint.setTextSize((int) mIndicatorTextSize);
        mIndicatorPaint.setStrokeWidth(mIndicatorWidth);
        mIndicatorPaint.setStyle(Paint.Style.FILL_STYLE);

        mScaleGestureDetector = new ScaleGestureDetector(getContext(), mScaleGestureListener);
        mGestureDetector = new GestureDetector(getContext(), mGestureListener);
        mScroller = new ScrollHelper();

        mRevealAnimator = ValueAnimator.ofFloat(0, 1);
        mRevealAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
            @Override
            public void onUpdate(AnimatorValue animatorValue, float value) {
                mRevealValue = value;
                mDrawMatrix.reset();
                mDrawMatrix.setScale(1, 1.f * mRevealValue, 0, mGraphHeight - mNegativeOffset);
                mGraph.invalidate();
            }
        });
        mRevealAnimator.setStateChangedListener(stateChangedListener);

        // The scroller doesn't have any built-in animation functions--it just supplies
        // values when we ask it to. So we have to have a way to call it every frame
        // until the fling ends. This code (ab)uses a ValueAnimator object to generate
        // a callback on every animation frame. We don't use the animated value at all.
        mScrollAnimator = ValueAnimator.ofFloat(0, 1);
        mScrollAnimator.setValueUpdateListener(new AnimatorValue.ValueUpdateListener() {
            @Override
            public void onUpdate(AnimatorValue animatorValue, float value) {
                tickScrollAnimation();
                invalidateGlobal();
            }
        });
    }

    private Animator.StateChangedListener stateChangedListener = new Animator.StateChangedListener() {
        @Override
        public void onStart(Animator animator) {

        }

        @Override
        public void onStop(Animator animator) {

        }

        @Override
        public void onCancel(Animator animator) {

        }

        @Override
        public void onEnd(Animator animator) {
            mStartedAnimation = false;
        }

        @Override
        public void onPause(Animator animator) {

        }

        @Override
        public void onResume(Animator animator) {

        }
    };

    float maxValue;
    float minValue;

    /**
     * Should be called after new data is inserted. Will be automatically called, when the view dimensions
     * changed.
     * <p>
     * Calculates various offsets and positions for different overlay features based on the graph settings.
     * After the calculation the Path is generated as a normal path or cubic path (Based on 'egUseCubic' attribute).
     */
    @Override
    protected void onDataChanged() {
        if (!mSeries.isEmpty()) {
            maxValue = 0.f;
            minValue = Float.MAX_VALUE;
            mNegativeValue = 0.f;
            mNegativeOffset = 0.f;
            mHasNegativeValues = false;
            calculMinAndMaxValue();

            float heightMultiplier = mUsableGraphHeight / (maxValue - minValue);
            calculateOffset(minValue, heightMultiplier);
            calculPath(minValue, heightMultiplier);
            if (calculateLegendBounds()) {
                Utils.calculateLegendInformation(getContext(), mSeries.get(0).getSeries(), 0, mGraphWidth, mLegendPaint);
            }
            calculValue();
        }

        super.onDataChanged();
    }

    private void calculMinAndMaxValue() {
        // calculate the maximum and minimum value present in data
        for (ValueLineSeries series : mSeries) {
            for (ValueLinePoint point : series.getSeries()) {
                if (point.getValue() > maxValue) {
                    maxValue = point.getValue();
                }

                if (point.getValue() < mNegativeValue) {
                    mNegativeValue = point.getValue();
                }

                if (point.getValue() < minValue) {
                    minValue = point.getValue();
                }
            }
        }

        // check if the standardvalue is greater than all other values
        if (mShowStandardValues) {
            for (StandardValue value : mStandardValues) {
                if (value.getValue() > maxValue) {
                    maxValue = value.getValue();
                }

                if (value.getValue() < mNegativeValue) {
                    mNegativeValue = value.getValue();
                }

                if (value.getValue() < minValue) {
                    minValue = value.getValue();
                }
            }
        }

        if (!mUseDynamicScaling) {
            minValue = 0;
        } else {
            minValue *= mScalingFactor;
        }

        // check if values below zero were found
        if (mNegativeValue < 0) {
            mHasNegativeValues = true;
            maxValue += (mNegativeValue * -1);
            minValue = 0;
        }
    }

    private void calculateOffset(float minValue, float heightMultiplier) {
        // calculate the offset
        if (mHasNegativeValues) {
            mNegativeOffset = (mNegativeValue * -1) * heightMultiplier;
        }

        // calculate the y position for standardValue
        if (mShowStandardValues) {
            for (StandardValue value : mStandardValues) {
                value.setY((int) (mGraphHeight - mNegativeOffset - ((value.getValue() - minValue) * heightMultiplier)));
            }
        }
    }

    private void calculValue() {
        // set the first point for the indicator
        if (mShowIndicator && mSeries.size() == 1) {
            int size = mSeries.get(0).getSeries().size();
            int index;

            // Only calculate if more than one point is available
            if (size > 1) {
                // position the indicator in the middle at the nearest value
                if (size == 3) {
                    index = size / 2;
                } else {
                    index = (size / 2) - 1;
                }

                mFocusedPoint = mSeries.get(0).getSeries().get(index);
                mTouchedArea = mFocusedPoint.getCoordinates();

                calculateValueTextHeight();
            }
        }
    }

    private void calculPath(float minValue, float heightMultiplier) {
        for (ValueLineSeries series : mSeries) {
            int seriesPointCount = series.getSeries().size();

            // check if more than one point is available
            if (seriesPointCount <= 1) {
                LogUtil.info(LOG_TAG, "More than one point should be available!");
            } else {
                float widthOffset = (float) mGraphWidth / (float) seriesPointCount;
                widthOffset += widthOffset / seriesPointCount;
                float currentOffset = 0;
                series.setWidthOffset(widthOffset);

                // used to store first point and set it later as ending point, if a graph fill is selected
                float firstX = currentOffset;
                float firstY = mGraphHeight - ((series.getSeries().get(0).getValue() - minValue) * heightMultiplier);

                Path path = new Path();
                path.moveTo(firstX, firstY);
                series.getSeries().get(0).setCoordinates(new Point2D(firstX, firstY));

                // If a cubic curve should be drawn then calculate cubic path
                // If not then just draw basic lines
                if (mUseCubic) {
                    Point2D p1 = new Point2D();
                    Point2D p2 = new Point2D();
                    Point2D p3 = new Point2D();

                    for (int i = 0; i < seriesPointCount - 1; i++) {
                        calculCubicTo(minValue, heightMultiplier, series, seriesPointCount, widthOffset, currentOffset, path, p1, p2, p3, i);
                        currentOffset += widthOffset;
                    }
                } else {
                    boolean first = true;
                    int count = 1;

                    for (ValueLinePoint point : series.getSeries()) {
                        if (first) {
                            first = false;
                            continue;
                        }
                        currentOffset += widthOffset;
                        if (count == seriesPointCount - 1) {
                            // if the last offset is smaller than the width, then the offset should be as long as the graph
                            // to prevent a graph drop
                            if (currentOffset < mGraphWidth) {
                                currentOffset = mGraphWidth;
                            }
                        }
                        point.setCoordinates(new Point2D(currentOffset, mGraphHeight - ((point.getValue() - minValue) * heightMultiplier)));
                        path.lineTo(point.getCoordinates().getX(), point.getCoordinates().getY());
                        count++;
                    }
                }

                if (mUseOverlapFill) {
                    path.lineTo(mGraphWidth, mGraphHeight);
                    path.lineTo(0, mGraphHeight);
                    path.lineTo(firstX, firstY);
                }

                series.setPath(path);
            }
        }
    }

    private void calculCubicTo(float minValue, float heightMultiplier, ValueLineSeries series, int seriesPointCount,
                float widthOffset, float currentOffset, Path path, Point2D p1, Point2D p2, Point2D p3, int num) {
        int i3 = (seriesPointCount - num) < 3 ? num + 1 : num + 2;
        float offset2 = (seriesPointCount - num) < 3 ? mGraphWidth : currentOffset + widthOffset;
        float offset3 = (seriesPointCount - num) < 3 ? mGraphWidth : currentOffset + (2 * widthOffset);

        p1.setX(currentOffset);
        p1.setY(mGraphHeight - ((series.getSeries().get(num).getValue() - minValue) * heightMultiplier));

        p2.setX(offset2);
        p2.setY(mGraphHeight - ((series.getSeries().get(num + 1).getValue() - minValue) * heightMultiplier));
        Utils.calculatePointDiff(p1, p2, p1, mSecondMultiplier);

        p3.setX(offset3);
        p3.setY(mGraphHeight - ((series.getSeries().get(i3).getValue() - minValue) * heightMultiplier));
        Utils.calculatePointDiff(p2, p3, p3, mFirstMultiplier);

        series.getSeries().get(num + 1).setCoordinates(new Point2D(p2.getX(), p2.getY()));
        path.cubicTo(new Point(p1.getX(), p1.getY()), new Point(p2.getX(), p2.getY()), new Point(p3.getX(), p3.getY()));
    }

    /**
     * Recalculate x coordinates *
     *
     * @param _GraphWidth graph width
     */
    private void recalculateXCoordinates(float _GraphWidth) {
        int seriesPointCount = mSeries.get(0).getSeries().size();
        float widthOffset = _GraphWidth / (float) seriesPointCount;
        widthOffset += widthOffset / seriesPointCount;
        float currentOffset = 0;
        for (ValueLinePoint point : mSeries.get(0).getSeries()) {
            point.getCoordinates().setX(currentOffset);
            currentOffset += widthOffset;
        }
    }

    /**
     * Calculate legend bounds boolean
     *
     * @return the boolean
     */
    private boolean calculateLegendBounds() {
        int index = 0;
        int size = mSeries.get(0).getSeries().size();

        // Only calculate if more than one point is available
        if (size > 1) {
            for (ValueLinePoint valueLinePoint : mSeries.get(0).getSeries()) {
                if (!(index == 0 || index == size - 1)) {
                    RectFloat rectFloat = new RectFloat(
                            valueLinePoint.getCoordinates().getX() - mSeries.get(0).getWidthOffset() / 2,
                            0,
                            valueLinePoint.getCoordinates().getX() + mSeries.get(0).getWidthOffset() / 2,
                            mLegendHeight);
                    valueLinePoint.setLegendBounds(rectFloat);
                } else {
                    valueLinePoint.setIgnore(true);
                }

                index++;
            }

            return true;
        } else {
            return false;
        }
    }

    /**
     * Calculates the legend bounds for a custom list of legends.
     */
    protected void onLegendDataChanged() {
        int legendCount = mLegendList.size();
        float margin;
        if (legendCount == 0) {
            margin = 0;
        } else {
            margin = (mGraphWidth / legendCount);
        }
        float currentOffset = 0;

        for (LegendModel model : mLegendList) {
            model.setLegendBounds(new RectFloat(currentOffset, 0, currentOffset + margin, mLegendHeight));
            currentOffset += margin;
        }

        Utils.calculateLegendInformation(getContext(), mLegendList, 0, mGraphWidth, mLegendPaint);

        invalidateGlobal();
    }

    /**
     * Calculates the text height for the indicator value and sets its x-coordinate.
     */
    private void calculateValueTextHeight() {
        Rect valueRect = new Rect();
        Rect legendRect = new Rect();
        String str = Utils.getFloatString(mFocusedPoint.getValue(), mShowDecimal) + (!mIndicatorTextUnit.isEmpty() ? " " + mIndicatorTextUnit : "");

        // calculate the boundaries for both texts
        valueRect = mIndicatorPaint.getTextBounds(str);
        legendRect = mLegendPaint.getTextBounds(mFocusedPoint.getLegendLabel());

        // calculate string positions in overlay
        mValueTextHeight = valueRect.getHeight();
        mValueLabelY = (int) (mValueTextHeight + mIndicatorTopPadding);
        mLegendLabelY = (int) (mValueTextHeight + mIndicatorTopPadding + legendRect.getHeight() + Utils.dpToPx(getContext(),7.f));

        int chosenWidth = valueRect.getWidth() > legendRect.getWidth() ? valueRect.getWidth() : legendRect.getWidth();

        // check if text reaches over screen
        if (mFocusedPoint.getCoordinates().getX() + chosenWidth + mIndicatorLeftPadding > -Utils.getTranslationX(mDrawMatrixValues) + mGraphWidth) {
            mValueLabelX = (int) (mFocusedPoint.getCoordinates().getX() - (valueRect.getWidth() + mIndicatorLeftPadding));
            mLegendLabelX = (int) (mFocusedPoint.getCoordinates().getX() - (legendRect.getWidth() + mIndicatorLeftPadding));
        } else {
            mValueLabelX = mLegendLabelX = (int) (mFocusedPoint.getCoordinates().getX() + mIndicatorLeftPadding);
        }
    }

    /**
     * Fling *
     *
     * @param velocityX velocity x
     * @param velocityY velocity y
     */
    private void fling(int velocityX, int velocityY) {
        mScroller.doFling(
                (int) -Utils.getTranslationX(mDrawMatrixValues),
                (int) -Utils.getTranslationY(mDrawMatrixValues),
                velocityX,
                velocityY,
                0, (int) calculateMaxTranslation(Utils.getScaleX(mDrawMatrixValues), mGraphWidth),
                0, (int) calculateMaxTranslation(Utils.getScaleY(mDrawMatrixValues), mGraphHeight));
        LogUtil.info(TAG, "fling startx: " + -Utils.getTranslationX(mDrawMatrixValues)
                + " startY: " + -Utils.getTranslationY(mDrawMatrixValues)
                + " maxX: " + calculateMaxTranslation(Utils.getScaleX(mDrawMatrixValues), mGraphWidth)
                + " maxY " + calculateMaxTranslation(Utils.getScaleY(mDrawMatrixValues), mGraphHeight));
        // Start the animator and tell it to animate for the expected duration of the fling.
        int splineFlingDuration = Utils.getSplineFlingDuration(Math.abs(velocityX), getContext());
        mScrollAnimator.setDuration(splineFlingDuration);
        mScrollAnimator.start();
    }

    /**
     * Tick scroll animation
     */
    private void tickScrollAnimation() {
        if (!mScroller.isFinished()) {
            mScroller.updateScroll();

            int currX = mScroller.getCurrValue(ScrollHelper.AXIS_X);
            int currY = mScroller.getCurrValue(ScrollHelper.AXIS_Y);

            LogUtil.info(TAG, "fling: tickScrollAnimation Width " + currX
                    + " mGraphHeight " + currY);
            if (currX > 0 && currX < calculateMaxTranslation(Utils.getScaleX(mDrawMatrixValues), mGraphWidth)) {
                mDrawMatrixValues[2] = -currX;
            }

            if (currY > 0 && currY < calculateMaxTranslation(Utils.getScaleY(mDrawMatrixValues), mGraphHeight)) {
                mDrawMatrixValues[5] = -currY;
            }

            mDrawMatrix.setElements(mDrawMatrixValues);
        } else {
            mScroller.abortAnimation();
        }
    }

    /**
     * Force a stop to all pie motion. Called when the user taps during a fling.
     */
    private void stopScrolling() {
        mScrollAnimator.cancel();
        mScroller.abortAnimation();
    }

    /**
     * Constrain view
     */
    public void constrainView() {
        mDrawMatrix.getElements(mDrawMatrixValues);

        // Check for minimum scale
        mDrawMatrixValues[0] = Math.max(1f, mDrawMatrixValues[0]);
        mDrawMatrixValues[4] = Math.max(1f, mDrawMatrixValues[4]);

        // Check for maximum scale
        mDrawMatrixValues[0] = Math.min(mMaxZoomX, mDrawMatrixValues[0]);
        mDrawMatrixValues[4] = Math.min(mMaxZoomY, mDrawMatrixValues[4]);

        // check for translation values so the graph wont be translated further as possible
        mDrawMatrixValues[2] = Math.min(0f, mDrawMatrixValues[2]);
        mDrawMatrixValues[5] = Math.min(0f, mDrawMatrixValues[5]);

        float scaleX = Utils.getScaleX(mDrawMatrixValues);
        float scaleY = Utils.getScaleY(mDrawMatrixValues);

        // check if maximum x-translation doesn't get too big
        float remainingTranslationX = (scaleX * mGraphWidth) + mDrawMatrixValues[2];
        if (remainingTranslationX < mGraphWidth) {
            mDrawMatrixValues[2] = -calculateMaxTranslation(scaleX, mGraphWidth);
        }

        // check if maximum y-translation doesn't get too big
        float remainingTranslationY = (scaleY * mGraphHeight) + mDrawMatrixValues[5];
        if (remainingTranslationY < mGraphHeight) {
            mDrawMatrixValues[5] = -calculateMaxTranslation(scaleY, mGraphHeight);
        }

        mDrawMatrix.setElements(mDrawMatrixValues);
    }

    /**
     * Calculate max translation float
     *
     * @param _Scale     scale
     * @param _Dimension dimension
     * @return the float
     */
    private float calculateMaxTranslation(float _Scale, float _Dimension) {
        return _Scale * _Dimension - _Dimension;
    }

    /**
     * Returns the first series.
     *
     * @return The first series.
     */
    @Override
    public List<ValueLinePoint> getData() {
        return mSeries.get(0).getSeries();
    }

    /**
     * Returns all series which are currently inserted.
     *
     * @return Inserted series.
     */
    public List<ValueLineSeries> getDataSeries() {
        return mSeries;
    }

    /**
     * On graph draw *
     *
     * @param _Canvas canvas
     */
    @Override
    protected void onGraphDraw(Canvas _Canvas) {
        super.onGraphDraw(_Canvas);
        if (mUseOverlapFill) {
            mLinePaint.setStyle(Paint.Style.FILL_STYLE);
        } else {
            mLinePaint.setStrokeWidth(mLineStroke);
            mLinePaint.setStyle(Paint.Style.STROKE_STYLE);
        }

        _Canvas.concat(mDrawMatrix);
        if (mHasNegativeValues) {
            _Canvas.translate(0, -mNegativeOffset);
        }

        // drawing of lines
        for (ValueLineSeries series : mSeries) {
            mLinePaint.setColor(new Color(series.getColor()));
            _Canvas.drawPath(series.getPath(), mLinePaint);
        }
    }

    /**
     * On graph overlay draw *
     *
     * @param _Canvas canvas
     */
    @Override
    protected void onGraphOverlayDraw(Canvas _Canvas) {
        super.onGraphOverlayDraw(_Canvas);

        // draw x-axis
        mLegendPaint.setStrokeWidth(mXAxisStroke);
        _Canvas.drawLine(
                new Point(0,
                        (mGraphHeight - mNegativeOffset) * Utils.getScaleY(mDrawMatrixValues) + Utils.getTranslationY(mDrawMatrixValues)),
                new Point(mGraphWidth,
                        (mGraphHeight - mNegativeOffset) * Utils.getScaleY(mDrawMatrixValues) + Utils.getTranslationY(mDrawMatrixValues)),
                mLegendPaint
        );

        if (containsPoints()) {
            // draw standard value line
            if (mShowStandardValues) {
                mIndicatorPaint.setPathEffect(mDashPathEffect);
                for (StandardValue value : mStandardValues) {
                    mIndicatorPaint.setColor(new Color(value.getColor()));
                    mIndicatorPaint.setStrokeWidth(value.getStroke());
                    _Canvas.drawLine(
                            new Point(0,
                                    value.getY() * Utils.getScaleY(mDrawMatrixValues) + Utils.getTranslationY(mDrawMatrixValues)),
                            new Point(mGraphWidth,
                                    value.getY() * Utils.getScaleY(mDrawMatrixValues) + Utils.getTranslationY(mDrawMatrixValues)),
                            mIndicatorPaint
                    );
                }
            }

            // draw touch indicator
            drawText(_Canvas);
        }
    }

    private void drawText(Canvas _Canvas) {
        if (mShowIndicator && mSeries.size() == 1) {
            mIndicatorPaint.setPathEffect(null);
            mIndicatorPaint.setColor(new Color(mIndicatorLineColor));
            mIndicatorPaint.setStrokeWidth(mIndicatorWidth);

            _Canvas.translate(Utils.getTranslationX(mDrawMatrixValues), 0);
            _Canvas.drawLine(new Point(mTouchedArea.getX(), 0), new Point(mTouchedArea.getX(), mGraphHeight), mIndicatorPaint);

            if (mFocusedPoint != null) {
                // set shadow
                if (mActivateIndicatorShadow) {
                    mIndicatorPaint.setMaskFilter(new MaskFilter(mIndicatorShadowStrength, MaskFilter.Blur.SOLID));
                    mIndicatorPaint.setColor(new Color(mIndicatorShadowColor));
                    _Canvas.drawText(mIndicatorPaint, Utils.getFloatString(mFocusedPoint.getValue(), mShowDecimal) + (!mIndicatorTextUnit.isEmpty() ? " " + mIndicatorTextUnit : ""),
                            mValueLabelX,
                            mValueLabelY
                    );
                    mIndicatorPaint.setMaskFilter(null);                    
                    LogUtil.info(TAG, "mActivateIndicatorShadow: ");
                }

                mIndicatorPaint.setColor(new Color(mIndicatorTextColor));
                _Canvas.drawText(mIndicatorPaint, Utils.getFloatString(mFocusedPoint.getValue(), mShowDecimal) + (!mIndicatorTextUnit.isEmpty() ? " " + mIndicatorTextUnit : ""),
                        mValueLabelX,
                        mValueLabelY
                );

                if (mShowLegendBeneathIndicator) {
                    mLegendPaint.setColor(new Color(mIndicatorTextColor));
                    _Canvas.drawText(mLegendPaint, mFocusedPoint.getLegendLabel(),
                            mLegendLabelX, mLegendLabelY
                    );
                }

                // reset shadow
                if (mActivateIndicatorShadow) {
                    LogUtil.info(TAG, "mActivateIndicatorShadow: ");
                }
            }
        }
    }

    /**
     * On legend draw *
     *
     * @param _Canvas canvas
     */
    @Override
    protected void onLegendDraw(Canvas _Canvas) {
        super.onLegendDraw(_Canvas);
        mLegendPaint.setColor(new Color(mLegendColor));
        mLegendPaint.setStrokeWidth(DEF_LEGEND_STROKE);

        if (!mSeries.isEmpty()) {
            _Canvas.translate(Utils.getTranslationX(mDrawMatrixValues), 0);

            if (mUseCustomLegend) {
                for (LegendModel model : mLegendList) {
                    RectFloat bounds = model.getLegendBounds();
                    _Canvas.drawText(mLegendPaint, model.getLegendLabel(), model.getLegendLabelPosition(), bounds.bottom - mMaxFontHeight);
                    _Canvas.drawLine(
                            new Point((bounds.left + bounds.right) * 0.5f,
                                    bounds.bottom - mMaxFontHeight * 2 - mLegendTopPadding),
                            new Point((bounds.left + bounds.right) * 0.5f,
                                    mLegendTopPadding), mLegendPaint);
                }
            } else {
                List<? extends BaseModel> list = mSeries.get(0).getSeries();
                for (BaseModel model : list) {
                    if (model.canShowLabel()) {
                        RectFloat bounds = model.getLegendBounds();
                        _Canvas.drawText(mLegendPaint, model.getLegendLabel(), model.getLegendLabelPosition(), bounds.bottom - mMaxFontHeight);
                        _Canvas.drawLine(
                                new Point((bounds.left + bounds.right) * 0.5f,
                                        bounds.bottom - mMaxFontHeight * 2 - mLegendTopPadding),
                                new Point((bounds.left + bounds.right) * 0.5f,
                                        mLegendTopPadding), mLegendPaint
                        );
                    }
                }
            }
        }
    }

    /**
     * On graph overlay touch event boolean
     *
     * @param _Event event
     * @return the boolean
     */
    @Override
    protected boolean onGraphOverlayTouchEvent(TouchEvent _Event) {
        super.onGraphOverlayTouchEvent(_Event);

        if (!mStartedAnimation && containsPoints()) {
            mScaleGestureDetector.onTouchEvent(_Event);
            mGestureDetector.onTouchEvent(_Event);

            float newX = getScaledXCoordinate(_Event.getPointerPosition(0).getX());
            float newY = _Event.getPointerPosition(0).getY();

            switch (_Event.getAction()) {
                case TouchEvent.PRIMARY_POINT_UP:
                    if (!mIsInteracting) {
                        findNearestPoint(newX, newY);
                    } else {
                        mIsInteracting = false;
                    }
                    return true;
            }
        }

        return true;
    }

    /**
     * Find nearest point *
     *
     * @param _X x
     * @param _Y y
     */
    private void findNearestPoint(float _X, float _Y) {
        if (mShowIndicator && mSeries.size() == 1) {
            int size = mSeries.get(0).getSeries().size();
            for (int i = 0; i < size; i++) {
                float pointX = mSeries.get(0).getSeries().get(i).getCoordinates().getX();

                // check if touchedX equals one the points
                if (pointX == _X) {
                    mFocusedPoint = mSeries.get(0).getSeries().get(i);
                    break;
                } else {
                    // if first point bigger than touched x select first
                    if (i == 0 && pointX > _X) {
                        mFocusedPoint = mSeries.get(0).getSeries().get(i);
                        break;
                    }
                    // check if we reached the last. if --> (true) use last point
                    else if (i == size - 1) {
                        mFocusedPoint = mSeries.get(0).getSeries().get(i);
                        break;
                    } else {
                        float nextX = mSeries.get(0).getSeries().get(i + 1).getCoordinates().getX();

                        // check if touchedX is between two points
                        if (_X > pointX && _X < nextX) {
                            // check which distance between touchedX and the two points is smaller
                            if (_X - pointX > nextX - _X) {
                                mFocusedPoint = mSeries.get(0).getSeries().get(i + 1);
                                break;
                            } else {
                                mFocusedPoint = mSeries.get(0).getSeries().get(i);
                                break;
                            }
                        }
                        // check if touchedX distance between the points is equal -> choose first Point
                        else if (_X > pointX && _X < nextX) {
                            mFocusedPoint = mSeries.get(0).getSeries().get(i);
                            break;
                        }
                    }
                }
            }

            if (mFocusedPoint != null) {
                mTouchedArea = mFocusedPoint.getCoordinates();

            } else {
                mTouchedArea.setX(_X);
                mTouchedArea.setY(_Y);
            }

            if (mLastPoint != mFocusedPoint) {
                mLastPoint = mFocusedPoint;

                calculateValueTextHeight();

                if (mListener != null) {
                    mListener.onPointFocused(mSeries.get(0).getSeries().indexOf(mFocusedPoint));
                }
            }

            invalidateGlobal();
        }
    }

    /**
     * Get scaled x coordinate float
     *
     * @param _X x
     * @return the float
     */
    private float getScaledXCoordinate(float _X) {
        return _X - Utils.getTranslationX(mDrawMatrixValues);
    }

    /**
     * Contains points boolean
     *
     * @return the boolean
     */
    private boolean containsPoints() {
        boolean result = false;
        for (ValueLineSeries sery : mSeries) {
            if (!sery.getSeries().isEmpty()) {
                result = true;
            }
        }
        return result;
    }


}
